// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.departures

import android.content.Context
import android.content.Intent
import android.net.ConnectivityManager
import android.net.ConnectivityManager.NetworkCallback
import android.net.Network
import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.os.Build
import android.os.Bundle
import android.os.CountDownTimer
import android.text.format.DateUtils
import android.text.format.DateUtils.MINUTE_IN_MILLIS
import android.view.View
import android.widget.Toast
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.datepicker.MaterialDatePicker
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.api.Error
import xyz.apiote.bimba.czwek.data.sources.Server
import xyz.apiote.bimba.czwek.databinding.ActivityDeparturesBinding
import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_GREY_OUT
import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_HIDE
import xyz.apiote.bimba.czwek.departures.BimbaDeparturesAdapter.Companion.TERMINUS_ARRIVAL_SHOWING_KEY
import xyz.apiote.bimba.czwek.repo.EventItem
import xyz.apiote.bimba.czwek.repo.Favourite
import xyz.apiote.bimba.czwek.repo.OfflineRepository
import xyz.apiote.bimba.czwek.repo.Stop
import xyz.apiote.bimba.czwek.units.Second
import xyz.apiote.bimba.czwek.units.Tim
import java.time.Instant
import java.time.LocalTime
import java.time.ZoneId
import java.time.ZonedDateTime

class DeparturesActivity : AppCompatActivity() {
	companion object {
		const val CODE_PARAM = "code"
		const val NAME_PARAM = "name"
		const val FEED_PARAM = "feedID"
		const val LINES_FILTER_PARAM = "linesFilter"
		const val LINE_PARAM = "line"
		const val EXACT_PARAM = "exact"

		fun getIntent(
			context: Context,
			code: String,
			name: String,
			feedID: String,
			exact: Boolean = false,
		)  = Intent(context, DeparturesActivity::class.java).apply {
			putExtra(CODE_PARAM, code)
			putExtra(NAME_PARAM, name)
			putExtra(FEED_PARAM, feedID)
			putExtra(EXACT_PARAM, exact)
		}

		fun getIntent(
			context: Context,
			code: String,
			name: String,
			feedID: String,
			lines: Array<String>
		) = Intent(context, DeparturesActivity::class.java).apply {
			putExtra(CODE_PARAM, code)
			putExtra(NAME_PARAM, name)
			putExtra(FEED_PARAM, feedID)
			putExtra(LINES_FILTER_PARAM, lines)
		}
	}

	private var _binding: ActivityDeparturesBinding? = null
	private val binding get() = _binding!!

	private lateinit var adapter: BimbaDeparturesAdapter

	private lateinit var snackbar: Snackbar

	private lateinit var viewModel: DeparturesViewModel

	private val datePicker =
		MaterialDatePicker.Builder.datePicker().setTitleText(R.string.title_select_date)
			.setNegativeButtonText(R.string.clear_date_selection)
			.build()
	private var timePickerStart: MaterialTimePicker? = null
	private var timePickerEnd: MaterialTimePicker? = null
	private var linePicker: MaterialAlertDialogBuilder? = null
	private val linesFilterTemporary = mutableMapOf<String, Boolean>()

	// TODO [elizabeth] millisInFuture from header Cache-Control max-age
	private val countdown =
		object : CountDownTimer(Second(30).milliseconds(), Tim(1).milliseconds()) {
			override fun onTick(millisUntilFinished: Long) {
				val timsUntillFinished = Tim(Second(millisUntilFinished.toDouble() / 1000))
				binding.departuresUpdatesProgress.progress = timsUntillFinished.tims.toInt()
			}

			override fun onFinish() {
				getDepartures()
			}
		}

	override fun onCreate(savedInstanceState: Bundle?) {
		enableEdgeToEdge()
		super.onCreate(savedInstanceState)
		_binding = ActivityDeparturesBinding.inflate(layoutInflater)
		setContentView(binding.root)

		ViewCompat.setOnApplyWindowInsetsListener(binding.departuresRecycler) { v, windowInsets ->
			windowInsets.displayCutout?.safeInsetRight?.let { v.updatePadding(right = it) }
			windowInsets.displayCutout?.safeInsetLeft?.let { v.updatePadding(left = it) }
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			v.updatePadding(bottom = insets.bottom)
			WindowInsetsCompat.CONSUMED
		}
		ViewCompat.setOnApplyWindowInsetsListener(binding.root) { v, windowInsets ->
			val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
			v.updatePadding(right = insets.right, left = insets.left)
			windowInsets
		}

		viewModel = ViewModelProvider(this)[DeparturesViewModel::class.java]
		viewModel.showingTerminusArrivals = PreferenceManager.getDefaultSharedPreferences(this)
			.getString(TERMINUS_ARRIVAL_SHOWING_KEY, TERMINUS_ARRIVAL_GREY_OUT)
			?: TERMINUS_ARRIVAL_GREY_OUT

		getLine()?.let {
			viewModel.mutableLinesFilter.value = mapOf(Pair(it, true))
		}

		getLines()?.associate { Pair(it, true) }?.let {
			if (it.isNotEmpty()) {
				viewModel.mutableLinesFilter.value = it
			}
		}

		linePicker = MaterialAlertDialogBuilder(this)
			.setTitle(resources.getString(R.string.title_select_line))
			.setNegativeButton(R.string.clear_date_selection) { _, _ ->
				viewModel.mutableLinesFilter.value = emptyMap()
				//getDepartures()
			}
			.setPositiveButton(R.string.ok) { _, _ ->
				viewModel.mutableLinesFilter.value = emptyMap()
				viewModel.mutableLinesFilter.value = linesFilterTemporary
				//getDepartures()
			}

		viewModel.linesFilter.observe(this) {
			// TODO if is before we got departures, do nothing
			val departures = viewModel.departures.value?.events ?: emptyList()
			updateItems(departures
				.filter { d ->
					it.values.all { !it } or (it[d.vehicle.Line.name] == true)
				}
				.filter { d ->
					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
				}
				.filter { d ->
					val t = LocalTime.of(d.filterTime().Hour.toInt(), d.filterTime().Minute.toInt())
					t >= viewModel.startTime && t <= viewModel.endTime
				}.map { EventItem(it) },
				null,
				true
			)
		}

		viewModel.departures.observe(this) { stopDepartures ->
			val items = mutableListOf<EventItem>()
			if (stopDepartures.alerts.isNotEmpty()) {
				items.add(EventItem(stopDepartures.alerts))
			}
			items.addAll(stopDepartures.events
				.filter { d ->
					viewModel.linesFilter.value?.let { filter ->
						filter.values.all { !it } or (filter[d.vehicle.Line.name] == true)
					} != false
				}
				.filter { d ->
					viewModel.showingTerminusArrivals != TERMINUS_ARRIVAL_HIDE || !d.terminusArrival
				}
				.filter { d ->
					val t = LocalTime.of(d.filterTime().Hour.toInt(), d.filterTime().Minute.toInt())
					t >= viewModel.startTime && t <= viewModel.endTime
				}.map { EventItem(it) })
			updateItems(items, stopDepartures.stop)
			viewModel.openBottomSheet?.departureID()?.let { adapter.get(it) }
				?.let { it.event?.let { departure -> viewModel.openBottomSheet?.update(departure) } }


			val lines = stopDepartures.events.map { it.vehicle.Line.name }.sortedWith { s1, s2 ->
				val s1n = s1.toIntOrNull()
				val s2n = s2.toIntOrNull()
				if (s1n != null && s2n != null) {
					s1.toInt() - s2.toInt()
				} else {
					s1.compareTo(s2)
				}
			}.toSet().toTypedArray()
			val selections =
				lines.map { viewModel.linesFilter.value?.getOrDefault(it, false) == true }.toBooleanArray()

			linePicker?.setMultiChoiceItems(lines, selections) { _, which, checked ->
				linesFilterTemporary[lines[which]] = checked
			}
		}
		viewModel.error.observe(this) {
			showError(it)
		}

		binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).isEnabled = false

		// TODO async esp. with Online
		if (runBlocking {
				val repository = OfflineRepository(this@DeparturesActivity)
				val f = repository.getFavourite(
					getCode() ?: ""
				)
				repository.close()
				f
			} != null) {
			binding.departuresAppBar.menu.findItem(R.id.favourite).setIcon(R.drawable.favourite_full)
		}

		datePicker.addOnNegativeButtonClickListener {
			viewModel.date = null
			viewModel.startTime = LocalTime.MIN
			viewModel.endTime = LocalTime.MAX
			binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).isEnabled = false
			getDepartures(true)
		}
		datePicker.addOnPositiveButtonClickListener {
			if (viewModel.date == null) {
				viewModel.startTime = LocalTime.MIN
				viewModel.endTime = LocalTime.MAX
			}
			viewModel.date = Instant.ofEpochMilli(it).atZone(ZoneId.systemDefault())
				.toLocalDate()
			binding.departuresAppBar.menu.findItem(R.id.departures_filter_bytime).isEnabled = true
			getDepartures(true)
		}

		binding.collapsingLayout.apply {
			title = getName()
			val tf = ResourcesCompat.getFont(this@DeparturesActivity, R.font.yellowcircle8)
			setCollapsedTitleTypeface(tf)
			setExpandedTitleTypeface(tf)
		}

		binding.departuresAppBar.setOnMenuItemClickListener {
			when (it.itemId) {
				R.id.favourite -> {
					if (!viewModel.linesFilter.value.isNullOrEmpty() && viewModel.linesFilter.value!!.any { filter -> filter.value }) {
						MaterialAlertDialogBuilder(this).setIcon(
							AppCompatResources.getDrawable(
								this,
								R.drawable.filter
							)
						)
							.setTitle(R.string.filtered_departures)
							.setMessage(R.string.filtered_stop_question)
							.setPositiveButton(R.string.filtered) { _, _ ->
								saveFavourite(viewModel.linesFilter.value!!.keys)
							}
							.setNegativeButton(R.string.unfiltered) { _, _ ->
								saveFavourite(setOf())
							}
							.setNeutralButton(R.string.cancel) { _, _ -> }
							.show()
					} else {
						saveFavourite(setOf())
					}
					true
				}

				R.id.departures_calendar -> {
					datePicker.show(supportFragmentManager, "datePicker")
					true
				}

				R.id.departures_filter_byline -> {
					linesFilterTemporary.clear()
					viewModel.linesFilter.value?.forEach { filter ->
						linesFilterTemporary[filter.key] = filter.value
					}
					linePicker?.show()
					true
				}

				R.id.departures_filter_bytime -> {
					timePickerStart =
						MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_start)
							.setTimeFormat(TimeFormat.CLOCK_24H)
							.setHour(viewModel.startTime.hour)
							.setMinute(viewModel.startTime.minute)
							.setNegativeButtonText(R.string.clear_date_selection)
							.build()
					timePickerEnd = MaterialTimePicker.Builder().setTitleText(R.string.title_select_time_end)
						.setTimeFormat(TimeFormat.CLOCK_24H)
						.setHour(viewModel.endTime.hour)
						.setMinute(viewModel.endTime.minute)
						.setNegativeButtonText(R.string.clear_date_selection)
						.build()
					timePickerEnd!!.addOnPositiveButtonClickListener {
						viewModel.endTime = LocalTime.of(timePickerEnd!!.hour, timePickerEnd!!.minute)
						getDepartures(true)
					}
					timePickerEnd!!.addOnNegativeButtonClickListener {
						viewModel.endTime = LocalTime.MAX
						getDepartures(true)
					}
					timePickerStart!!.addOnPositiveButtonClickListener {
						viewModel.startTime = LocalTime.of(timePickerStart!!.hour, timePickerStart!!.minute)
						timePickerEnd!!.show(supportFragmentManager, "timePickerEnd")
					}
					timePickerStart!!.addOnNegativeButtonClickListener {
						viewModel.startTime = LocalTime.MIN
						timePickerEnd!!.show(supportFragmentManager, "timePickerEnd")
					}
					timePickerStart!!.show(supportFragmentManager, "timePickerStart")
					true
				}

				R.id.terminus_arrival_showing -> {
					val options = arrayOf(
						TERMINUS_ARRIVAL_GREY_OUT,
						TERMINUS_ARRIVAL_HIDE,
						BimbaDeparturesAdapter.TERMINUS_ARRIVAL_SHOW
					)
					var selected = viewModel.showingTerminusArrivals!!
					MaterialAlertDialogBuilder(this)
						.setTitle(R.string.terminus_arrival_showing)
						.setIcon(R.drawable.terminus)
						.setSingleChoiceItems(
							options,
							options.indexOf(viewModel.showingTerminusArrivals)
						) { _, i ->
							selected = options[i]
						}
						.setPositiveButton(R.string.ok) { _, _ ->
							viewModel.showingTerminusArrivals = selected
							adapter.showingTerminusArrivals = selected
							getDepartures()
						}
						.setNegativeButton(R.string.cancel) { _, _ -> }
						.show()
					true
				}

				else -> super.onOptionsItemSelected(it)
			}
		}

		binding.departuresRecycler.layoutManager = LinearLayoutManager(this)
		binding.departuresRecycler.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
		binding.departuresRecycler.itemAnimator = null
		binding.departuresRecycler.addOnScrollListener(
			object : RecyclerView.OnScrollListener() {
				override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
					super.onScrolled(recyclerView, dx, dy)
					val llm = binding.departuresRecycler.layoutManager as LinearLayoutManager
					val dataLength = adapter.itemCount
					if (llm.findLastCompletelyVisibleItemPosition() == dataLength - 1) {
						if (!viewModel.allItemsRequested) {
							viewModel.requestedItemsNumber += 12
							getDepartures()
						}
					}
				}
			}
		)
		adapter = BimbaDeparturesAdapter(layoutInflater, this, listOf()) {
			DepartureBottomSheet(it).apply {
				show(supportFragmentManager, DepartureBottomSheet.TAG)
				viewModel.openBottomSheet = this
				setOnCancel { viewModel.openBottomSheet = null }
			}
		}
		adapter.showingTerminusArrivals = viewModel.showingTerminusArrivals!!
		binding.departuresRecycler.adapter = adapter
		WindowCompat.setDecorFitsSystemWindows(window, false)

		snackbar = Snackbar.make(binding.root, "", Snackbar.LENGTH_INDEFINITE)

		val networkCallback: NetworkCallback = object : NetworkCallback() {
			override fun onAvailable(network: Network) {
				getDepartures()
			}

			override fun onLost(network: Network) {
			}
		}

		val connectivityManager = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
			connectivityManager.registerDefaultNetworkCallback(networkCallback)
		} else {
			val request = NetworkRequest.Builder()
				.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET).build()
			connectivityManager.registerNetworkCallback(request, networkCallback)
		}
	}

	override fun onResume() {
		super.onResume()
		getDepartures()
		viewModel.openBottomSheet?.show(supportFragmentManager, DepartureBottomSheet.TAG)
	}

	override fun onPause() {
		viewModel.openBottomSheet?.dismiss()
		super.onPause()
		countdown.cancel()
	}

	override fun onStop() {
		super.onStop()
		countdown.cancel()
	}

	private fun getName(): String {
		return when (intent?.action) {
			Intent.ACTION_VIEW -> getString(R.string.stop_from_qr_code)
			null -> intent?.extras?.getString(NAME_PARAM) ?: ""
			else -> ""
		}
	}

	private fun getLine(): String? {
		return when (intent?.action) {
			null -> intent?.extras?.getString(LINE_PARAM)
			else -> null
		}
	}

	private fun getLines(): List<String>? {
		return when (intent?.action) {
			null -> intent?.extras?.getStringArray(LINES_FILTER_PARAM)?.toList()
			else -> null
		}
	}

	private fun getCode() = intent?.extras?.getString(CODE_PARAM)

	private fun getExact() = intent?.extras?.getBoolean(EXACT_PARAM) == true

	fun getDepartures(force: Boolean = false) {
		binding.departuresUpdatesProgress.isIndeterminate = true
		if (force) {
			showLoading()
		} else {
			adapter.refreshItems()
			setupSnackbar()
		}
		viewModel.getDepartures(this, viewModel.date, force, getExact())
	}

	private fun setupSnackbar() {
		val lastUpdateAgo = ZonedDateTime.now().toEpochSecond() - adapter.lastUpdate.toEpochSecond()
		if (lastUpdateAgo > 59 && adapter.lastUpdate.year != 0) {
			snackbar.setText(
				getString(
					R.string.last_update,
					DateUtils.getRelativeTimeSpanString(
						adapter.lastUpdate.toEpochSecond() * 1000,
						ZonedDateTime.now().toEpochSecond() * 1000,
						MINUTE_IN_MILLIS,
						DateUtils.FORMAT_ABBREV_RELATIVE
					)
				)
			).show()
		} else {
			snackbar.dismiss()
		}
	}

	private fun showError(error: Error) {
		binding.departuresProgress.visibility = View.GONE
		binding.departuresRecycler.visibility = View.GONE
		binding.departuresUpdatesProgress.visibility = View.GONE
		binding.errorImage.visibility = View.VISIBLE
		binding.errorText.visibility = View.VISIBLE

		binding.errorText.text = getString(error.stringResource)
		binding.errorImage.setImageDrawable(
			AppCompatResources.getDrawable(
				this,
				error.imageResource
			)
		)
	}

	private fun showLoading() {
		binding.departuresOverlay.visibility = View.VISIBLE
		binding.departuresProgress.visibility = View.VISIBLE
		binding.errorImage.visibility = View.GONE
		binding.errorText.visibility = View.GONE
	}

	private fun updateItems(
		departures: List<EventItem>,
		stop: Stop?,
		leaveAlert: Boolean = false
	) {
		setupSnackbar()
		if (adapter.itemCount == 0) {
			binding.departuresRecycler.scrollToPosition(0)
		}
		binding.departuresProgress.visibility = View.GONE
		// TODO [elizabeth] max, progress from header Cache-Control max-age
		binding.departuresUpdatesProgress.apply {
			visibility = View.VISIBLE
			isIndeterminate = false
			max = Tim(Second(30)).tims.toInt()
			progress = Tim(Second(30)).tims.toInt()
		}
		countdown.cancel()
		countdown.start()
		adapter.update(departures, viewModel.date != null, true, leaveAlert)
		binding.collapsingLayout.apply {
			stop?.let { title = it.stopName }
		}
		if (departures.isEmpty()) {
			binding.errorImage.visibility = View.VISIBLE
			binding.errorText.visibility = View.VISIBLE
			binding.departuresRecycler.visibility = View.GONE

			binding.errorText.text = getString(R.string.no_departures)
			binding.errorImage.setImageDrawable(
				AppCompatResources.getDrawable(
					this, R.drawable.error_search
				)
			)
		} else {
			if (departures.size < viewModel.requestedItemsNumber) {
				viewModel.allItemsRequested = true
			}
			binding.departuresOverlay.visibility = View.GONE
			binding.errorImage.visibility = View.GONE
			binding.errorText.visibility = View.GONE
			binding.departuresRecycler.visibility = View.VISIBLE
		}
		// todo [3.2; traffic] stop info
	}

	private fun saveFavourite(linesFilter: Set<String>) {
		val context = this
		val feedID = intent.extras?.getString(FEED_PARAM)
		val code = intent?.extras?.getString(CODE_PARAM)
		if (feedID == null || code == null) {
			Toast.makeText(this, R.string.cannot_save_favourite, Toast.LENGTH_LONG).show()
			return
		}
		binding.departuresAppBar.menu.findItem(R.id.favourite).setIcon(R.drawable.favourite_full)
		MainScope().launch {
			val repo = OfflineRepository(context)
			val feedName = repo.getFeeds(context, Server.get(context))?.get(feedID)?.name
			if (feedName == null) {
				Toast.makeText(context, R.string.cannot_save_favourite, Toast.LENGTH_LONG).show()
				return@launch
			}
			val favourite = (repo.getFavourite(code) ?: Favourite(
				null,
				feedID,
				feedName,
				code,
				getName(),
				linesFilter.toList(),
				getExact()
			)).copy(lines = linesFilter.toList())
			repo.saveFavourite(favourite)
			repo.close()
		}
	}
}
